package com.katesoft.scale4j.persistent.spring.postprocessor;

import static com.katesoft.scale4j.common.services.IBeanNameReferences.SESSION_FACTORY;

import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.metadata.ClassMetadata;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;

import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.model.InternalDomainEntity;
import com.katesoft.scale4j.persistent.spring.EntityManagedOrNativeHibernateFactoryBean;

/**
 * this post processor will store domain classes information into dedicated table's meta data table.
 * <p/>
 * this information can be useful for some plsql function(they can detect if schema's table is
 * tracked by application or not).
 * <p/>
 * NOTE: this post processor can be disabled(if at least defined in spring context) by setting
 * enabled flag to false(enabled by default).
 * 
 * @author kate2007
 */
public class InternalDomainPostProcessor implements BeanPostProcessor {
   private final Logger logger = LogFactory.getLogger(getClass());
   private boolean enabled = true;

   @Override
   public Object postProcessBeforeInitialization(Object bean,
            @SuppressWarnings("unused") String beanName) throws BeansException {
      return bean;
   }

   @Override
   public Object postProcessAfterInitialization(final Object bean, String beanName)
            throws BeansException {
      if (enabled) {
         if (EntityManagedOrNativeHibernateFactoryBean.class.isAssignableFrom(bean.getClass())
                  && SESSION_FACTORY.equals(beanName)) {
            Object object = ((EntityManagedOrNativeHibernateFactoryBean) bean).getObject();
            SessionFactory sessionFactory = (object instanceof SessionFactory) ? ((SessionFactory) object)
                     : ((HibernateEntityManagerFactory) object).getSessionFactory();
            final Configuration configuration = ((EntityManagedOrNativeHibernateFactoryBean) bean)
                     .getConfiguration();
            Map<String, ClassMetadata> metadata = sessionFactory.getAllClassMetadata();
            final Iterator<Entry<String, ClassMetadata>> iterator = metadata.entrySet().iterator();
            HibernateTemplate template = new HibernateTemplate(sessionFactory);
            template.execute(new HibernateCallback<Object>() {
               @SuppressWarnings({ "unchecked" })
               @Override
               public Object doInHibernate(Session session) throws HibernateException, SQLException {
                  Transaction transaction = session.beginTransaction();
                  try {
                     List<InternalDomainEntity> existingEntities = session.createCriteria(
                              InternalDomainEntity.class).list();
                     List<InternalDomainEntity> newEntities = new LinkedList<InternalDomainEntity>();
                     while (iterator.hasNext()) {
                        Entry<String, ClassMetadata> entry = iterator.next();
                        PersistentClass persistentClass = configuration.getClassMapping(entry
                                 .getKey());
                        InternalDomainEntity internalDomainEntity = new InternalDomainEntity();
                        internalDomainEntity.setEntityName(entry.getKey());
                        internalDomainEntity.setEntityClass(persistentClass.getClassName());
                        internalDomainEntity.setTableName(persistentClass.getTable().getName());
                        InternalDomainEntity existingDefinition = findExistingDefinition(
                                 existingEntities, internalDomainEntity);
                        if (existingDefinition == null) {
                           session.save(internalDomainEntity);
                           logger.debug("saving %s",
                                    ReflectionToStringBuilder.toString(internalDomainEntity));
                        } else {
                           if (!EqualsBuilder.reflectionEquals(existingDefinition,
                                    internalDomainEntity)) {
                              existingDefinition.setEntityClass(internalDomainEntity
                                       .getEntityClass());
                              existingDefinition.setEntityName(internalDomainEntity.getEntityName());
                              existingDefinition.setTableName(internalDomainEntity.getTableName());
                              logger.debug("updating %s",
                                       ReflectionToStringBuilder.toString(existingDefinition));
                              session.update(existingDefinition);
                           }
                        }
                        newEntities.add(internalDomainEntity);
                     }
                     session.flush();
                     Collection<InternalDomainEntity> orphanEntities = findOrphanEntities(
                              existingEntities, newEntities);
                     if (!orphanEntities.isEmpty()) {
                        logger.debug("removing orphan internal domain entities %s", orphanEntities);
                     }
                     for (InternalDomainEntity orphanEntity : orphanEntities) {
                        session.delete(orphanEntity);
                     }
                     session.flush();
                     transaction.commit();
                  } catch (Exception e) {
                     transaction.rollback();
                     logger.error(e);
                  }
                  return null;
               }

               private Collection<InternalDomainEntity> findOrphanEntities(
                        List<InternalDomainEntity> existingCollection,
                        List<InternalDomainEntity> newCollection) {
                  Collection<InternalDomainEntity> orphan = new LinkedHashSet<InternalDomainEntity>();
                  for (InternalDomainEntity internalDomainEntity : existingCollection) {
                     if (findExistingDefinition(newCollection, internalDomainEntity) == null) {
                        orphan.add(internalDomainEntity);
                     }
                  }
                  return orphan;
               }

               private InternalDomainEntity findExistingDefinition(
                        List<InternalDomainEntity> entities, InternalDomainEntity actual) {
                  for (InternalDomainEntity entity : entities) {
                     if (entity.getEntityName().equalsIgnoreCase(actual.getEntityName())) {
                        return entity;
                     }
                  }
                  return null;
               }
            });
         }
      }
      return bean;
   }

   public void setEnabled(boolean enabled) {
      this.enabled = enabled;
   }
}
